//! ## Client C APIs for Command Queue in CS2 System
//!
//! This module provides C APIs for the command queue in the CS2 system.

use std::{ffi, slice};

use cs2s::{
    cache::{CacheTableKeyRef, CacheTableVal},
    command::CommandQueueClientHandle,
    sign::CodeSign,
};

use crate::retval;

/// Command queue client handle pointer.
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CommandQueueClientHandlePtr(*mut ffi::c_void);

/// Safety: The pointer can be transferred between threads.
unsafe impl Send for CommandQueueClientHandlePtr {}

/// Create a command queue client.
///
/// ## Safety
///
/// The provided pointers must be valid, otherwise it causes undefined behavior.
/// - `name` must be a valid pointer to a null-terminated UTF-8 C string.
/// - `handle_ptr` must be a valid pointer.
///
/// ## Returns
///
/// - `0` on success.
/// - `-EINVAL` if the name is invalid.
///
/// The error code could also come from the underlying implementation, i.e. from
/// [`CommandQueueClientHandle::new`].
#[export_name = "cs2s_cmdq_create"]
pub unsafe extern "C" fn create(
    name: *const ffi::c_char,
    handle_ptr: *mut CommandQueueClientHandlePtr,
) -> ffi::c_int {
    if name.is_null() {
        return -libc::EINVAL;
    }

    // Safety: The name is not null and guaranteed to be a valid C string by the caller.
    let Ok(name) = unsafe { ffi::CStr::from_ptr(name) }.to_str() else {
        return -libc::EINVAL;
    };

    let queue = retval::check!(CommandQueueClientHandle::new(name));

    // Safety: The handle pointer is guaranteed to be a valid pointer by the caller.
    unsafe {
        handle_ptr.write(CommandQueueClientHandlePtr(
            Box::into_raw(Box::new(queue)) as _
        ));
    }

    0
}

/// Connect to the server.
///
/// ## Safety
///
/// The provided pointer must be valid, otherwise it causes undefined behavior.
/// - `queue` must be created by [`create`].
///
/// Caller should also ensure that no data races occur, i.e. no other threads are calling
/// this function on the same queue.
#[export_name = "cs2s_cmdq_connect"]
pub unsafe extern "C" fn connect(queue: CommandQueueClientHandlePtr) -> ffi::c_int {
    // Safety: The queue is guaranteed to be created by the C API.
    let queue = unsafe { &mut *(queue.0 as *mut CommandQueueClientHandle) };

    retval::check!(queue.connect());

    0
}

/// Attach paths to the server.
///
/// ## Safety
///
/// The provided pointers must be valid, otherwise it causes undefined behavior.
/// - `queue` must be created by [`create`].
/// - `paths` must be null or a valid array of UTF-8 C strings.
/// - `paths_len` must be the length of the array `paths`.
///
/// ## Returns
///
/// - `0` on success.
/// - `-EINVAL` if the argument `paths` is invalid:
///   - If `paths` is not null, but the array contains a null pointer.
///   - If `paths` is not null, but the C string is not valid UTF-8.
///
/// The error code could also come from the underlying implementation, i.e. from
/// [`CommandQueueClientHandle::paths_attach`].
#[export_name = "cs2s_cmdq_paths_attach"]
pub unsafe extern "C" fn paths_attach(
    queue: CommandQueueClientHandlePtr,
    paths: *const *const ffi::c_char,
    paths_len: usize,
) -> ffi::c_int {
    // Safety: The queue is guaranteed to be created by the C API.
    let queue = unsafe { &*(queue.0 as *mut CommandQueueClientHandle) };

    let paths = if paths.is_null() {
        return 0;
    } else {
        let mut p = Vec::with_capacity(paths_len);

        for i in 0..paths_len {
            // Safety: The pointer is guaranteed to be valid by the caller.
            if unsafe { paths.add(i).read() }.is_null() {
                return -libc::EINVAL;
            }
            // Safety: The pointer is guaranteed to point to a valid C string by the caller.
            let Ok(path) = unsafe { ffi::CStr::from_ptr(paths.add(i).read()) }.to_str() else {
                return -libc::EINVAL;
            };
            p.push(path);
        }

        p
    };

    retval::check!(queue.paths_attach(paths));

    0
}

/// Synchronize cache to the server.
///
/// ## Safety
///
/// The provided pointers must be valid, otherwise it causes undefined behavior.
/// - `queue` must be created by [`create`].
/// - `guest_sign` must be created by [`calc_sign`](../helper/fn.calc_sign.html).
/// - `path` must be a valid pointer to a null-terminated UTF-8 C string.
/// - `host_meta` and `host_code` must be valid pointers.
/// - `host_meta_len` and `host_code_len` must be the length of the corresponding code.
///
/// ## Returns
///
/// - `0` on success.
/// - `-EINVAL` if the argument is invalid:
///   - If `path` is null, or the C string is not valid UTF-8.
///   - If `guest_sign` or `host_meta` or `host_code` is null.
///
/// The error code could also come from the underlying implementation, i.e. from
/// [`CommandQueueClientHandle::synchronize`].
#[export_name = "cs2s_cmdq_sync"]
pub unsafe extern "C" fn synchronize(
    queue: CommandQueueClientHandlePtr,
    path: *const ffi::c_char,
    guest_addr: usize,
    guest_size: usize,
    guest_sign: *const CodeSign,
    host_meta: *const ffi::c_void,
    host_meta_len: usize,
    host_code: *const ffi::c_void,
    host_code_len: usize,
) -> ffi::c_int {
    if path.is_null() || guest_sign.is_null() || host_meta.is_null() || host_code.is_null() {
        return -libc::EINVAL;
    }
    // Safety: The pointer is guaranteed to be a valid C string by the caller.
    let Ok(path) = unsafe { ffi::CStr::from_ptr(path) }.to_str() else {
        return -libc::EINVAL;
    };

    // Safety: The queue is guaranteed to be created by the C API.
    let queue = unsafe { &*(queue.0 as *mut CommandQueueClientHandle) };

    // Safety: The `guest_sign` is guaranteed to be valid by the caller.
    let guest_sign = unsafe { &*guest_sign };

    // Safety:
    // - The `host_meta` is guaranteed to be valid by the caller.
    // - The `host_meta_len` is guaranteed to be the length of the code by the caller.
    let host_meta = unsafe { slice::from_raw_parts(host_meta as *const u8, host_meta_len) };

    // Safety:
    // - The `host_code` is guaranteed to be valid by the caller.
    // - The `host_code_len` is guaranteed to be the length of the code by the caller.
    let host_code = unsafe { slice::from_raw_parts(host_code as *const u8, host_code_len) };

    retval::check!(queue.synchronize(
        path,
        &CacheTableKeyRef {
            guest_addr,
            guest_size,
            guest_sign
        },
        &CacheTableVal {
            host_meta,
            host_code
        }
    ));

    0
}

/// Destroy the command queue client.
///
/// ## Safety
///
/// The provided pointer must be valid, otherwise it causes undefined behavior.
/// - `queue` must be created by [`create`].
///
/// After calling this function, the `queue` should not be used.
#[export_name = "cs2s_cmdq_destroy"]
pub unsafe extern "C" fn destroy(queue: CommandQueueClientHandlePtr) {
    // Safety: The queue is guaranteed to be created by the C API.
    drop(unsafe { Box::from_raw(queue.0 as *mut CommandQueueClientHandle) });
}

#[cfg(test)]
mod test {
    use std::{ffi::CString, mem, ptr, thread};

    use coverage_helper::test;
    use cs2s::{
        cache::CacheTableKey,
        command::{Command, CommandKind, CommandQueueServerHandle, TerminateStatus},
        utils::map::Key,
    };
    use cs2s_testdata::*;

    use super::*;

    #[test]
    fn basic_collaboration() {
        const NAME: &str = "ctest-basic-colloboration";

        // Create a server.
        let server = CommandQueueServerHandle::new(NAME);
        assert!(server.is_ok());
        let mut server = server.unwrap();

        // Create a client via C API.
        // Safety: the name and the handle pointer is valid.
        let client = unsafe {
            let mut cmdq = mem::zeroed();
            let name = CString::new(NAME).unwrap();
            let result = create(name.as_ptr(), &mut cmdq);
            assert_eq!(result, 0);
            cmdq
        };

        let client_handle = thread::spawn(move || {
            // Send commands to the server.

            // Safety: The client is created by the C API.
            assert_eq!(unsafe { connect(client) }, 0);

            let mut path_array = [ptr::null(); PATHS.len()];
            let paths = PATHS
                .iter()
                .map(|&s| CString::new(s).unwrap())
                .collect::<Vec<_>>();
            for (i, path) in paths.iter().enumerate() {
                path_array[i] = path.as_ptr();
            }

            assert_eq!(
                // Safety: The client is created by the C API, and the paths are valid.
                unsafe { paths_attach(client, path_array.as_ptr(), PATHS.len()) },
                0
            );

            for (i, ((path, guest_code), (host_meta, host_code))) in PATHS
                .iter()
                .zip(GUEST_CODES.iter())
                .zip(HOST_METAS.iter().zip(HOST_CODES.iter()))
                .enumerate()
            {
                let path = CString::new(*path).unwrap();

                assert_eq!(
                    // Safety: The client is created by the C API, and the paths & codes
                    // are valid.
                    unsafe {
                        synchronize(
                            client,
                            path.as_ptr(),
                            0x400000 + i * 0x40,
                            guest_code.len(),
                            &guest_code.into(),
                            host_meta.as_ptr() as _,
                            host_meta.len(),
                            host_code.as_ptr() as _,
                            host_code.len(),
                        )
                    },
                    0
                );
            }

            // Safety: The client is created by the C API.
            unsafe { destroy(client) };

            assert!(CommandQueueClientHandle::terminate(NAME, TerminateStatus::Success).is_ok());
        });

        let server_handle = thread::spawn(move || {
            // Listen to the commands from the client.
            let mut index = 0;
            let index_max = 3 + GUEST_CODES.len();
            let result = server.listen(|command: &Command, payload: Vec<&[u8]>| {
                assert!(index < index_max);
                match index {
                    0 => {
                        assert_eq!(command.kind(), &CommandKind::Connect);
                        assert!(payload.is_empty());
                    }
                    1 => {
                        assert_eq!(
                            command.kind(),
                            &CommandKind::PathsAttach {
                                paths_len: PATHS.iter().map(|s| s.len()).sum::<usize>()
                                    + PATHS.iter().count()
                                    - 1,
                            }
                        );
                        assert_eq!(
                            payload[..],
                            PATHS.iter().map(|s| s.as_bytes()).collect::<Vec<_>>()
                        );
                    }
                    i if i > 1 && i < index_max - 1 => {
                        let j = i - 2;
                        assert_eq!(
                            command.kind(),
                            &CommandKind::Synchronize {
                                path_len: PATHS[j].len(),
                                cache_key: CacheTableKey::from_ref(&CacheTableKeyRef {
                                    guest_addr: 0x400000 + j * 0x40,
                                    guest_size: GUEST_CODES[j].len(),
                                    guest_sign: &GUEST_CODES[j].into(),
                                }),
                                host_meta_size: HOST_METAS[j].len(),
                                host_code_size: HOST_CODES[j].len(),
                            }
                        );
                        assert_eq!(
                            payload[..],
                            [PATHS[j].as_bytes(), HOST_METAS[j], HOST_CODES[j]],
                        );
                    }
                    _ => {
                        assert_eq!(command.kind(), &CommandKind::Disconnect);
                        assert!(payload.is_empty());
                    }
                }
                index += 1;
                Ok(())
            });
            assert!(result.is_ok());
        });

        // Wait for the threads to finish.
        assert!(client_handle.join().is_ok());
        assert!(server_handle.join().is_ok());
    }

    #[test]
    fn no_path_attached() {
        const NAME: &str = "ctest-no-path-attached";

        // Create a server.
        let server = CommandQueueServerHandle::new(NAME);
        assert!(server.is_ok());

        // Create a client via C API.
        // Safety: the name and the handle pointer is valid.
        let client = unsafe {
            let mut cmdq = mem::zeroed();
            let name = CString::new(NAME).unwrap();
            let result = create(name.as_ptr(), &mut cmdq);
            assert_eq!(result, 0);
            cmdq
        };

        // Send commands to the server.
        // Safety: The client is created by the C API.
        assert_eq!(unsafe { connect(client) }, 0);

        // Safety: The client is created by the C API.
        assert_eq!(unsafe { paths_attach(client, ptr::null(), 0) }, 0);

        // Safety: The client is created by the C API.
        unsafe { destroy(client) };
    }

    #[test]
    fn invalid_name_error() {
        // Invalid name with a null character.
        //
        // Safety: The name is invalid, so we expect an error return.
        assert!(unsafe { create(ptr::null(), ptr::null_mut()) } < 0);

        // Invalid name with a non-UTF-8 character.
        assert!(
            // Safety: The name is invalid, so we expect an error return.
            unsafe {
                create(
                    [b'c', 0xFF, b't', b'e', b's', b't', 0].as_ptr() as _,
                    ptr::null_mut(),
                )
            } < 0
        );

        // Invalid name with slashes.
        //
        // Safety: The name is invalid, so we expect an error return.
        assert!(unsafe { create(b"na/me\0".as_ptr() as _, ptr::null_mut()) } < 0);
    }

    #[test]
    fn invalid_paths() {
        const NAME: &str = "ctest-invalid-paths";

        // Create a server.
        let server = CommandQueueServerHandle::new(NAME);
        assert!(server.is_ok());

        // Create a client via C API.
        // Safety: the name and the handle pointer is valid.
        let client = unsafe {
            let mut cmdq = mem::zeroed();
            let name = CString::new(NAME).unwrap();
            let result = create(name.as_ptr(), &mut cmdq);
            assert_eq!(result, 0);
            cmdq
        };

        // Invalid paths with a null pointer.
        let paths = [ptr::null()];

        // Safety: The client is created by the C API and the paths are invalid.
        assert!(unsafe { paths_attach(client, paths.as_ptr(), paths.len()) } < 0);
        assert!(
            // Safety: The client is created by the C API.
            unsafe {
                synchronize(
                    client,
                    ptr::null(),
                    0,
                    GUEST_CODE_0.len(),
                    &GUEST_CODE_0.into(),
                    HOST_META_0.as_ptr() as _,
                    HOST_META_0.len(),
                    HOST_CODE_0.as_ptr() as _,
                    HOST_CODE_0.len(),
                )
            } < 0
        );

        // Invalid paths with a non-UTF-8 character.
        let path = [b'c', 0xFF, b't', b'e', b's', b't', 0];
        let paths = [path.as_ptr()];
        // Safety: The client is created by the C API and the paths are invalid.
        assert!(unsafe { paths_attach(client, paths.as_ptr() as _, paths.len()) } < 0);

        // Safety: The client is created by the C API.
        assert!(unsafe {
            synchronize(
                client,
                path.as_ptr() as _,
                0,
                GUEST_CODE_0.len(),
                &GUEST_CODE_0.into(),
                HOST_META_0.as_ptr() as _,
                HOST_META_0.len(),
                HOST_CODE_0.as_ptr() as _,
                HOST_CODE_0.len(),
            ) < 0
        });
    }
}
